之前的多模態我自己的電腦跑不動,所以接著我想把 API 移到 colab 上跑。
參考 ngrok 文件,有提供在 colab 上運行的說明。
https://ngrok.com/docs/using-ngrok-with/googleColab/
https://pyngrok.readthedocs.io/en/latest/integrations.html#google-colaboratory
先把 pyngrok 裝起來。
pip install pyngrok
接著一樣要輸入 ngrok 的 token。
from pyngrok import conf
conf.get_default().auth_token = "your ngrok token"
接著使用文件的範例跑跑看(我只改了一點點)。
import os
import threading
from flask import Flask
from pyngrok import ngrok
app = Flask(__name__)
port = "5000"
# Open a ngrok tunnel to the HTTP server
public_url = ngrok.connect(port).public_url
print( f" * ngrok tunnel   {public_url} -> http://127.0.0.1:{port}")
# Update any base URLs to use the public ngrok URL
app.config["BASE_URL"] = public_url
# ... Update inbound traffic via APIs to use the public-facing ngrok URL
# Define Flask routes
@app.route("/")
def index():
    return "Hello from Colab!"
# Start the Flask server in a new thread
threading.Thread(target=app.run, kwargs={"use_reloader": False}).start()
會看到輸出長這樣,這個 https://xxx.app 就是我們要的 url 了。
- ngrok tunnel https://xxx.app -> http://127.0.0.1:5000
 - Serving Flask app 'main'
 - Debug mode: off
 
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production >deployment. Use a production WSGI server instead.- Running on http://127.0.0.1:5000
 
INFO:werkzeug:Press CTRL+C to quit
連上看看,第一次連線會看到提示,按下 Visit Site 就好。

接下來先把它關掉,因為是用 Thread 開的,有點麻煩,我直接重啟工作階段比較快。
來跟 line bot 結合試試。
先把 SDK 裝起來。
pip install line-bot-sdk
調整一下 ECHO 範例,運行看看。
這邊我把範例給的 threading.Thread 的寫法換掉,畢竟要除錯重開都很麻煩
跑完記的去 Line Developers 裡更新 Messaging API,每次 ngrok 都會給新的 url。
import threading
from pyngrok import conf, ngrok
conf.get_default().auth_token = "your ngrok token"
from flask import Flask, request, abort
from linebot.v3 import (
    WebhookHandler
)
from linebot.v3.exceptions import (
    InvalidSignatureError
)
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    ReplyMessageRequest,
    TextMessage
)
from linebot.v3.webhooks import (
    MessageEvent,
    TextMessageContent
)
app = Flask(__name__)
configuration = Configuration(access_token='YOUR_CHANNEL_ACCESS_TOKEN')
handler = WebhookHandler('YOUR_CHANNEL_SECRET')
@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']
    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)
    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        app.logger.info("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)
    return 'OK'
@handler.add(MessageEvent, message=TextMessageContent)
def handle_message(event):
    with ApiClient(configuration) as api_client:
        line_bot_api = MessagingApi(api_client)
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=event.message.text)]
            )
        )
port = "5000"
public_url = ngrok.connect(port).public_url
print( f" * ngrok tunnel   {public_url} -> http://127.0.0.1:{port}")
#threading.Thread(target=app.run, kwargs={"use_reloader": False}).start()
if __name__ == "__main__":
    app.run(debug=False,host='localhost',port=port)

接著是我昨天漏寫了接收使用者傳影像時的處理方式,主要就是要多 import MessagingApiBlob、ImageMessageContent,參考下方程式
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    MessagingApiBlob,
    ReplyMessageRequest,
    StickerMessage,
    TextMessage
)
from linebot.v3.webhooks import (
    MessageEvent,
    TextMessageContent,
    ImageMessageContent
)
然後再多加一個 handler,用來接收影像訊息。
這部分的程式當收到影像時,會把影像存下來,然後回個貼圖。
@handler.add(MessageEvent, message=ImageMessageContent)
def handle_message_img(event):
    with ApiClient(configuration) as api_client:
        line_bot_blob_api = MessagingApiBlob(api_client)
        message_content = line_bot_blob_api.get_message_content(message_id=event.message.id)
        with open(f'{event.message.id}.jpg', 'wb') as fd:
            fd.write(message_content)          
        line_bot_api = MessagingApi(api_client)
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[StickerMessage(
                    package_id='1',
                    sticker_id='1')
                ]
            )
        )
接下來串串看多模態模型。
參照 Day 2 把 VisualGLM-6B 的環境建起來。
串起來試試。
from pyngrok import conf
conf.get_default().auth_token = "your ngrok token"
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("THUDM/visualglm-6b", trust_remote_code=True)
model = AutoModel.from_pretrained("THUDM/visualglm-6b", trust_remote_code=True).half().cuda()
from pyngrok import ngrok
import json
from flask import Flask, request, abort
from linebot.v3 import (
    WebhookHandler
)
from linebot.v3.exceptions import (
    InvalidSignatureError
)
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    MessagingApiBlob,
    ReplyMessageRequest,
    TextMessage
)
from linebot.v3.webhooks import (
    MessageEvent,
    ImageMessageContent
)
app = Flask(__name__)
configuration = Configuration(access_token='YOUR_CHANNEL_ACCESS_TOKEN')
handler = WebhookHandler('YOUR_CHANNEL_SECRET')
history = []
@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']
    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)
    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        app.logger.info("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)
    return 'OK'
@handler.add(MessageEvent, message=ImageMessageContent)
def handle_message_img(event):
    global history
    with ApiClient(configuration) as api_client:
        line_bot_blob_api = MessagingApiBlob(api_client)
        message_content = line_bot_blob_api.get_message_content(message_id=event.message.id)
        with open(f'{event.message.id}.jpg', 'wb') as fd:
            fd.write(message_content)
        image_path = f'{event.message.id}.jpg'
        response, history = model.chat(tokenizer, image_path, "描述這張圖片", history)
        line_bot_api = MessagingApi(api_client)
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=response)]
            )
        )
port = "5000"
public_url = ngrok.connect(port).public_url
print( f" * ngrok tunnel   {public_url} -> http://127.0.0.1:{port}")
if __name__ == "__main__":
    app.run(debug=False,host='localhost',port=port)

不過這樣沒有辦法沒有圖對話,不太好用,乾脆順便換個方式。
環境都不變,多一點步驟而已,先重啟工作階段。
把原始碼 clone 下來。
!git clone https://github.com/THUDM/VisualGLM-6B.git
進到資料夾中。
cd VisualGLM-6B
把模型載下來。
import argparse
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True)
from model import chat, VisualGLMModel
model, model_args = VisualGLMModel.from_pretrained('visualglm-6b', args=argparse.Namespace(fp16=True, skip_init=True))
from sat.model.mixins import CachedAutoregressiveMixin
model.add_mixin('auto-regressive', CachedAutoregressiveMixin())
使用模型的方式有一點點不同,另外現在有兩個 handler,分別處理文字跟影像訊息。
from pyngrok import conf
conf.get_default().auth_token = "your ngrok token"
from pyngrok import ngrok
import json
from flask import Flask, request, abort
from linebot.v3 import (
    WebhookHandler
)
from linebot.v3.exceptions import (
    InvalidSignatureError
)
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    MessagingApiBlob,
    ReplyMessageRequest,
    TextMessage
)
from linebot.v3.webhooks import (
    MessageEvent,
    TextMessageContent,
    ImageMessageContent
)
app = Flask(__name__)
configuration = Configuration(access_token='YOUR_CHANNEL_ACCESS_TOKEN')
handler = WebhookHandler('YOUR_CHANNEL_SECRET')
history = []
cache_image = None
@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']
    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)
    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        app.logger.info("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)
    return 'OK'
@handler.add(MessageEvent, message=TextMessageContent)
def handle_message(event):
    global history, cache_image
    with ApiClient(configuration) as api_client:
        line_bot_api = MessagingApi(api_client)
        response, history, cache_image = chat(None, model, tokenizer, event.message.text, history=history, image=cache_image)
        print(response)
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=response)]
            )
        )
@handler.add(MessageEvent, message=ImageMessageContent)
def handle_message_img(event):
    global history, cache_image
    with ApiClient(configuration) as api_client:
        line_bot_blob_api = MessagingApiBlob(api_client)
        message_content = line_bot_blob_api.get_message_content(message_id=event.message.id)
        with open(f'{event.message.id}.jpg', 'wb') as fd:
            fd.write(message_content)
        image_path = f'{event.message.id}.jpg'
        response, history, cache_image = chat(image_path, model, tokenizer, "描述這張圖片", history=history)
        line_bot_api = MessagingApi(api_client)
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=response)]
            )
        )
port = "5000"
public_url = ngrok.connect(port).public_url
print( f" * ngrok tunnel   {public_url} -> http://127.0.0.1:{port}")
#threading.Thread(target=app.run, kwargs={"use_reloader": False}).start()
if __name__ == "__main__":
    app.run(debug=False,host='localhost',port=5000)
跑完一樣記的去 Line Developers 更新 url。
最後試用看看。
不過這有點吃資源,吃了快 15G 的 GPU RAM。